#region Copyright (c) 2007-2008, Chris Klochek

/********************************************************************************************************************
'
' Copyright (c) 2007-2008, Chris Klochek
' All rights reserved.
' 
' Redistribution and use in source and binary forms, with or without modification, are permitted provided
' that the following conditions are met:
' 
' * Redistributions of source code must retain the above copyright notice, this list of conditions and the
' 	following disclaimer.
' 
' * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
' 	the following disclaimer in the documentation and/or other materials provided with the distribution.
' 
' * Neither the name of the author nor the names of its contributors may be used to endorse or 
' 	promote products derived from this software without specific prior written permission.
' 
' THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
' WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
' PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
' ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
' LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
' INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
' OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
' IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'
'*******************************************************************************************************************/

#endregion

using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.IO;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;
using System.Runtime.Remoting;
using System.Diagnostics;
using System.Threading;

namespace SuedeTest.CLRunner {
   class Program {
      private static string _testFileName;
      private static string _exePathAndName;
      private static string _optionalArgs = "";

      private static readonly string PROCESS_HOST = "SuedeTest.ProcessHost.exe";
      private static readonly string TEMP_EXE_NAME = "__oldexe.exe";

      private static readonly string IPC_PROTOCOL_NAME = "ipc";
      private static readonly string IPC_PORT_NAME = "SuedeTest";
      private static readonly string IPC_OBJECT_URI = "TextBridge";

      private static int _procReturnCode = 0;
      private static Process _testingProcess;
      private static IpcChannel _ipcChannel;
      private static SuedeTest.ProcessHost.ProcessLink _processBridge;
      private static ManualResetEvent _processDoneSignal;

      private static string GetStartupProjectAssemblyName() {
         return _exePathAndName;
      }

      private static string GetStartupProjectWorkingDir() {
         return _exePathAndName.Substring(0, _exePathAndName.LastIndexOf('\\'));
      }

      private static bool ParseCommandLine(string[] args) {
         if (args.Length > 1) {
            _exePathAndName = FullPathForFile(args[0]);
            _testFileName = FullPathForFile(args[1]);

            bool convertNext = false;
            for (int i = 2; i < args.Length; i++) {
               if (convertNext) {
                  args[i] = FullPathForFile(args[i]);
                  convertNext = false;
               }
               _optionalArgs += " " + args[i];

               if (args[i] == "-spec" || args[i] == "-libs") {
                  convertNext = true;
               }
            }
            return true;
         }
         return false;
      }

      private static string FullPathForFile(string fileName) {
         FileInfo fi = new FileInfo(fileName);
         return fi.FullName;
      }

      /// <summary>
      /// Launch a recorder/runner process, with the supplied arguments.
      /// Other standard args/bookkeeping that is common to launching procs occurs here.
      /// </summary>
      /// <param name="procName"></param>
      /// <param name="args"></param>
      private static void LaunchProcess(string procName, string args) {
         string startupExeFullFileName = GetStartupProjectAssemblyName();
         string projDir = GetStartupProjectWorkingDir();
         string pluginLocation = Assembly.GetExecutingAssembly().Location;

         if (string.IsNullOrEmpty(startupExeFullFileName) ||
            string.IsNullOrEmpty(projDir) ||
            string.IsNullOrEmpty(pluginLocation)) {
            return;
         }

         pluginLocation = pluginLocation.Substring(0, pluginLocation.LastIndexOf(@"\"));

         //app.exe -> __oldexe.exe
         //procname.exe -> app.exe
         //we'll restore the names properly after the process has exited
         File.Copy(startupExeFullFileName, projDir + @"\" + TEMP_EXE_NAME, true);
         File.Copy(pluginLocation + @"\" + procName, startupExeFullFileName, true);

         _testingProcess = new System.Diagnostics.Process();
         _testingProcess.EnableRaisingEvents = true;
         _testingProcess.Exited += new EventHandler(ProcessExitedCallback);

         string procArgs = projDir + @"\" + TEMP_EXE_NAME + " " +
                              pluginLocation + " " + args;

         _testingProcess.StartInfo = new System.Diagnostics.ProcessStartInfo(startupExeFullFileName, procArgs);
         _testingProcess.StartInfo.WorkingDirectory = projDir;
         _testingProcess.Start();
      }

      /// <summary>
      /// Called when the testing process (recorder or runner) has exited.
      /// Restores original file names.
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private static void ProcessExitedCallback(object sender, EventArgs e) {

         _procReturnCode = _testingProcess.ExitCode;

         _testingProcess.Close();

         _testingProcess.Dispose();

         //FIXME: why aren't all file handles associated an exited process already
         //closed?  We can race here with RestoreFiles, as the copy/delete ops will fail
         //because a process (the exited one :P) is still apparently up and running.
         System.Threading.Thread.Sleep(500);

         ShutdownIPC();

         RestoreFiles();

         _processDoneSignal.Set();
      }

      private static void ShutdownIPC() {
         if (_ipcChannel != null) {
            ChannelServices.UnregisterChannel(_ipcChannel);
            _ipcChannel = null;
         }

         if (_processBridge != null) {
            _processBridge = null;
         }
      }

      private static void RestoreFiles() {
         string projDir = GetStartupProjectWorkingDir();

         try {
            File.Copy(projDir + @"\" + TEMP_EXE_NAME, GetStartupProjectAssemblyName(), true);
            File.Delete(projDir + @"\" + TEMP_EXE_NAME);
         } catch (System.IO.IOException) {
         }
      }

      /// <summary>
      /// Creates an IPC channel for this process, hosting a single "textbridge" that other processes can 
      /// post strings to.
      /// </summary>
      private static void CreateIPCBridge() {
         BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider();
         serverProvider.TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full;
         System.Collections.IDictionary props = new System.Collections.Hashtable();
         props["name"] = IPC_PROTOCOL_NAME;
         props["portName"] = IPC_PORT_NAME;
         props["exclusiveAddressUse"] = "false";
         _ipcChannel = new IpcChannel(props, new BinaryClientFormatterSinkProvider(), serverProvider);

         ChannelServices.RegisterChannel(_ipcChannel, false);

         RemotingConfiguration.RegisterWellKnownServiceType(typeof(SuedeTest.ProcessHost.ProcessLink), IPC_OBJECT_URI, WellKnownObjectMode.Singleton);

         _processBridge = (SuedeTest.ProcessHost.ProcessLink)Activator.GetObject(typeof(SuedeTest.ProcessHost.ProcessLink),
            IPC_PROTOCOL_NAME + "://" + IPC_PORT_NAME + "/" + IPC_OBJECT_URI);
      }

      static int Main(string[] args) {
         
         if (!ParseCommandLine(args)) {
            Console.Error.Write("Unable to launch command-line runner.  Insufficient number of args.");
            return -1;
         }

         _processDoneSignal = new ManualResetEvent(false);

         CreateIPCBridge();

         _processBridge.LogUpdated += new SuedeTest.ProcessHost.ProcessLink.LogUpdatedDelegate(UpdateTestResults);

         string procArgs = IPC_PROTOCOL_NAME + "://" + IPC_PORT_NAME + "/" + IPC_OBJECT_URI + " -testfile " + _testFileName + _optionalArgs;
         
         try {
            LaunchProcess(PROCESS_HOST, procArgs);
         } catch (Exception ex) {
            Console.Error.Write("Unable to launch host process: " + ex);

            EnsureProcessDestroyed();

            RestoreFiles();
            
            ShutdownIPC();

            _processDoneSignal.Set();

            return 1;
         }
         
         _processDoneSignal.WaitOne();

         return _procReturnCode;
      }

      private static void EnsureProcessDestroyed() {
         if (ProcessExists ()) {
            if (!_testingProcess.HasExited) {
               try {
                  _testingProcess.Kill();
               } catch { }
            }
            try {
               _testingProcess.Close();
               _testingProcess.Dispose();
            } catch {
            } finally {
               _testingProcess = null;
            }
         }
      }

      private static bool ProcessExists() {
         if (_testingProcess != null) {
            try {
               if (_testingProcess.Handle != IntPtr.Zero) {
                  return true;
               }
            } catch { }
         }

         return false;
      }

      static void UpdateTestResults(string newText) {
         if (newText.StartsWith("ERR:")) {
            Console.Error.Write(newText.Substring(4));
         } else {
            Console.Out.Write(newText);
         }
      }
   }
}